在了解了抽象化的概念後,接下來我們要討論的是物件導向中的第三個特性:多型 (Polymorphism)。
昨天介紹抽象類別時,我們曾提到有時候繼承同一個父類別的子類別雖然有著類似的功能,但實際的執行方法卻各不一樣
來說明讓函式抽象化的好處。而物件導向的多型概念便是這想法的延伸,它的主要思想便是我們可以透過子類別讓從父類別繼承的函式出現變化,允許我們使用同樣的功能以不同的方式來完成。
舉例來說,讓我們再次回顧鴨子跟老鷹類別的例子,我們之前的做法是把它們簡單粗暴的把共同擁有的鳴叫行為放到父類別鳥類類型中,再把不同的部分 (游泳跟飛行函式) 各自放在子類別中。可是,這樣的做法有一點問題:
1. 我們把原本是相同目的的行為拆開到不同的子類別中
當我們重新看鴨子跟老鷹類別的游泳跟飛行行為時,我們可以發現它們都可以被歸類為相同目的的行為 - 移動。而我們因為實際執行方式不同的原因而把移動行為拆開分別放到兩個子類別後,便使鳥類類型的定義缺少了一個行為。如果我們預期後來新增的子類別,例如雞類別,也會有同樣目的的行為的話,那麼讓它們的移動行為重新整合在一起會是比較好的做法。
2. 假如我們沒辦法在使用前確定目標的類別是什麼時,我們將無法使用這些不同的部分
有些時候我們不一定能在宣告變數的時間點便知道它確實的類別是什麼,這時候我們可以使用父類別進行宣告來讓我們可以較靈活地在後續的部分建立繼承了該父類別的子類別。但這時候我們會面對一個問題就是雖然實際的物件類別是子類別,但我們只能根據父類別的定義來使用變數及函數。例子如下:[C#]
using System;
// 父類別
public class Bird {
public string name; // 共同擁有的變數
public void speak() { // 共同擁有的函式
Console.WriteLine(name + " Quack!");
}
}
public class Duck : Bird { // 繼承 Bird 類別
public Duck() { // 建構子
name = "Duck"; // 存取並修改來自父類別的變數
}
public void swim() { // Duck 類別中獨特的函式
Console.WriteLine(name + " Swim!");
}
}
public class Eagle : Bird { // 繼承 Bird 類別
public Eagle() { // 建構子
name = "Eagle"; // 存取並修改來自父類別的變數
}
public void fly() { // Eagle 類別中獨特的函式
Console.WriteLine(name + " Fly!");
}
}
public class SuperClassExample
{
public static void Main(string[] args)
{
Bird someBird, otherBird; // 我們不肯定它們是什麼類別,因此先以 Bird 類別進行宣告
someBird = new Eagle(); // 建立 Eagle 類別的物件
someBird.speak(); // 顯示:Eagle Quack!
someBird.fly(); // 錯誤! 即使 someBird 是 Eagle 類別的物件,但程式沒辦法從宣告的 Bird 類別的定義中找到 fly 函式
otherBird = new Duck(); // 建立 Duck 類別的物件
otherBird.speak(); // 顯示:Duck Quack!
otherBird.swim(); // 錯誤! 即使 otherBird 是 Duck 類別的物件,但程式沒辦法從宣告的 Bird 類別的定義中找到 swim 函式
}
}
為了解決上面提到的這個問題,多型的想法為我們提供了一個選擇,那就是在子類別中覆寫 (Override) 父類別的行為。覆寫的做法跟我們昨天實作介面為沒有內容的函式描述補充實際內容的方法非常相似,只是這一次是把父類別中的函式的內容以自身類別的內容將其取代。例如在上面的例子中,我們可以重新把相同目的的游泳跟飛行行為整合為移動行為,並在父類別中準備一個基本的內容,再在子類別中按需求來修改函式的內容。
以下是實作的例子:[C#]
using System;
// 父類別
public class Bird {
public string name;
public void speak() {
Console.WriteLine(name + " Quack!");
}
public virtual void move() { // 把 Duck 類別跟 Eagle 類別的移動行為整合在一起
Console.WriteLine(name + " Move!");
}
}
public class Duck : Bird { // 繼承 Bird 類別
public Duck() { // 建構子
name = "Duck";
}
public override void move() { // 把 move 函式覆寫為 Duck 類別獨有的版本
Console.WriteLine(name + " Swim!");
}
}
public class Eagle : Bird { // 繼承 Bird 類別
public Eagle() { // 建構子
name = "Eagle";
}
public override void move() { // 把 move 函式覆寫為 Eagle 類別獨有的版本
Console.WriteLine(name + " Fly!");
}
}
public class OverrideExample
{
public static void Main(string[] args)
{
Bird someBird, otherBird; // 我們不肯定它們是什麼類別,因此先以 Bird 類別進行宣告
someBird = new Eagle(); // 建立 Eagle 類別的物件
someBird.speak(); // 顯示:Eagle Quack!
someBird.move(); // 顯示:Eagle Fly!
otherBird = new Duck(); // 建立 Duck 類別的物件
otherBird.speak(); // 顯示:Duck Quack!
otherBird.move(); // 顯示:Duck Swim!
}
}
由於我們在 Bird 類別中定義了 move 函式,因此這次我們便順利的讓以 Bird 類別進行宣告的 Eagle 類別物件跟 Duck 類別物件進行了飛行跟游泳的行為。而這樣做的另一個好處是我們可以確定在後續更新中新增的子類別也會有一個基本的移動行為:[C#]
...
public class Chicken : Bird { // 新增一個子類別 Chicken 類別
public Chicken() { // 建構子
name = "Chicken";
}
}
public class AnotherOverrideExample
{
public static void Main(string[] args)
{
Bird someBird, otherBird, newBird;
someBird = new Eagle();
someBird.speak();
someBird.move();
otherBird = new Duck();
otherBird.speak();
otherBird.move();
newBird = new Chicken(); // 建立 Chicken 類別的物件
newBird.speak(); // 顯示:Chicken Quack!
newBird.move(); // 顯示:Chicken Move!
}
}
多型的概念並不是只有在物件導向中出現,另一個體現了多型想法的行為是經常跟覆寫一同被提到的多載 (Overload)。不同的是,覆寫讓函式在子類別中出現變化,而多載則是讓函式在同一個類別中出現變化。多載的實現方式是透過使用不同的資料型態跟數量的参數來使程式把相同名稱的函式視為不同的函式,例子如下:[C#]
using System;
public class Duck {
public string name;
public Duck() {
name = "Duck";
}
public void speak() { // 多載:第一個 speak 函式 - 沒有參數
Console.WriteLine(name + ": Quack!");
}
public void speak(string content) { // 多載:第二個 speak 函式 - 一個 string 參數
Console.WriteLine(name + ": " + content);
}
}
public class OverloadExample
{
public static void Main(string[] args)
{
Duck duck = new Duck();
duck.speak(); // 由於沒有參數,因此使用了第一個 speak 函式,顯示: "Duck: Quack!"
duck.speak("Suba Suba!"); // 由於有一個 string 參數,因此使用了第二個 speak 函式,顯示: "Duck: Suba Suba!"
}
}
即使我們使用相同數量的參數,只要參數的資料型態不一樣也可以進行多載:[C#]
using System;
public class OverloadExample2
{
public static int Add(int num1, int num2) { // 多載:第一個 Add 函式 - 兩個 int 參數
return num1 + num2;
}
public static string Add(string str1, string str2) { // 多載:第二個 Add 函式 - 兩個 string 參數
return String.Concat(str1, str2);
}
public static void Main(string[] args)
{
Console.WriteLine(Add(1, 1)); // 由於有兩個 int 參數,因此使用了第一個 Add 函式,顯示: 2
Console.WriteLine(Add("1", "1")); // 由於有兩個 string 參數,因此使用了第二個 Add 函式,顯示: 11
}
}
今天,我們討論了有關多型的概念與它的兩種不同的呈現方式:一種是在物件導向中配合繼承使用的覆寫行為、另一種則是在同一個類別中使用的多載行為。而無論是哪一種的多型行為,它們都讓類別中的函式增添了更多的變化。